/**
 * 
 */
package org.tuestudent.grouppa;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.StringWriter;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.util.Queue;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.InflaterInputStream;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.tuestudent.grouppa.AppContext.ActiveScreens;
import org.tuestudent.grouppa.UiController.taskID;

import android.util.JsonReader;

/**
 * @author s121170
 * 
 */
public class TCPClient implements Runnable {

    private static Charset charsetCoding = Charset.forName("UTF-8");
    private static MCrypt encrypter = new MCrypt();

    private static HttpClient httpclient;

    private static Queue<MessageFromApp> outboundMessageQueue =
            new ConcurrentLinkedQueue<MessageFromApp>();
    private static Queue<MessageFromServer> inboundMessageQueue =
            new ConcurrentLinkedQueue<MessageFromServer>();

    // TODO: set this to server URL hard codedConnection-specific DNS Suffix:
     private static String serverURL =
             // android emulator to localhost
     //        "http://192.168.56.1:8080/GrouppaWeb/application/grouppaMain.php";
             "http://kxstilburg.eu/school/GrouppaWeb/grouppaMain.php";
     //        "http://10.87.92.62:8080/GrouppaWeb/application/grouppaMain.php";

    private static ExecutorService threadPool;

    private static String username;
    private static String password;
    private static String accessToken;

    private static MessageFromServer errorMessage;

    // hidden constructor, use getInstance()
    private TCPClient() {
        // CHECK: create an executorService with 1 thread
        threadPool = Executors.newFixedThreadPool(1);
        HttpParams httpParameters = new BasicHttpParams();
        HttpConnectionParams.setConnectionTimeout(httpParameters, 5000);
        HttpConnectionParams.setSoTimeout(httpParameters, 10000);

        httpclient = new DefaultHttpClient(httpParameters);

        errorMessage = new MessageFromServer();
        errorMessage.id = -1;
        errorMessage.fromController = "Application";
        errorMessage.messageType = "error";
        errorMessage.responseTold = -1;
        errorMessage.statusOK = false;
        errorMessage.toController = "error";
        errorMessage.toTask = "error";
    }

    private static TCPClient instance = null;

    public static TCPClient getInstance() {
        if (instance != null)
            return instance;

        instance = new TCPClient();
        return instance;
    }

    /**
     * puts the message in queue to be sent to server
     * 
     * @param message
     *            message to be sent
     */
    public void sendMessage(MessageFromApp message) {
        // CHECK: put the message into queue
        outboundMessageQueue.add(message);
        // CHECK: put the call to run() method to thread queue
        threadPool.execute(this);
    }
    
    /**
     * turn to false during connection retries to cancel further tries
     */
    public volatile boolean retry = true;
    private boolean errorShown = false;
    /**
     * Sends all encoded in JSON messages to the server
     * 
     * @param messages
     *            encoded in JSON messages to be sent
     * @throws Exception
     */
    private void sendAllMessages(String messages) throws Exception {
        // CHECK: send messages in compressed, encrypted form
        AppContext ctx = Main.getContext();
        byte[] encryptedCompressedMessage = compressEncryptMessage(messages);
        HttpPost httppost = new HttpPost(serverURL);

        httppost.setEntity(new ByteArrayEntity(encryptedCompressedMessage));
        httppost.setHeader("Content-Type", "application/octet-stream");
        HttpResponse response = null;
        errorShown = false;
        PeriodicUpdater updater = Main.getUpdater();
        if(updater != null)
            updater.pause = true;
        retry = true;
        while(true)
        {
            try{
                response = httpclient.execute(httppost);
                break;
            }
            catch(Exception ex)
            {
                if(!errorShown)
                    showConnectionError();
                errorShown =  true;
                if(!retry)
                {
                    if(updater != null)
                        updater.stop();
                    
                    outboundMessageQueue.clear();
                    logout();
                    connectingFinish();
                    return;
                }
            }
        }
        // CHECK: retrieve responses
        HttpEntity entity = response.getEntity();
        // NOTE: application will ignore any other contents than
        // application/octet-stream
        if (entity.getContentType().getValue()
                .equalsIgnoreCase("application/octet-stream")) {

            int length = (int) entity.getContentLength();
            if (length == 0) {
                entity.consumeContent();
                connectingFinish();
                return;
            }
            byte[] readBuffer = new byte[length];
            int position = 0;
            InputStream contentStream = entity.getContent();

            while (position < length) {
                // unblocking read content
                position +=
                        contentStream.read(readBuffer, position, length
                                - position);
                if (position >= length)
                    break;
                // blocking read content
                readBuffer[position++ ] = (byte) contentStream.read();
            }
            // we have content in readBuffer here, we decompress it
            String responseMessages = decryptDecompressMessages(readBuffer);

            retrieveMessages(responseMessages);
            
        }
        connectingFinish();
    }

    private void connectingFinish()
    {
        PeriodicUpdater updater = Main.getUpdater();
        httpclient.getConnectionManager().closeExpiredConnections();
        if(updater != null)
            updater.pause = false;
        if(errorShown)
        {
            UiController.addTask(taskID.clearDialogs, null);
            errorShown = false;
        }
        
    }
    private void logout() {
        AppContext ctx = Main.getContext();
        if(ctx.activeScreen == ActiveScreens.grouppaScreen)
        {
            ctx.grouppaActivity.finish();
        }
        
    }

    public volatile String connectionErrorText;
    private void showConnectionError() {
        UiController.addTask(taskID.connectionError, "I am sorry, but we experienced problem with connecting to the server, we are currently retrying connection. Press cancel to stop");
    }

    /**
     * splits messages , creates MessageFromServer from them and sends them to
     * controllers
     * 
     * @param responseMessages
     * @throws JSONException
     */
    public void retrieveMessages(String responseMessages) throws JSONException {
        // CHECK: split messages and create a MessageFromServer for each one of
        // them
        JSONArray marray = new JSONArray(responseMessages);
        int mnumber = marray.length();
        for (int i = 0; i < mnumber; i++ ) {
            JSONObject jobject = marray.optJSONObject(i);
            if (jobject != null) {
                MessageFromServer smessage =
                        MessageFromServer.fromJSONObject(jobject);
                sendResponseMessage(smessage);
            }
        }
    }

    private void sendResponseMessage(MessageFromServer smessage) {
        AppContext ctx = Main.getContext();
        ViewController controller;
        if(!smessage.toController.isEmpty())
        {
            controller = Main.getController(smessage.toController);
        }
        else
        {
            controller = ctx.getCurrentViewController();
        }
        controller.inboundMessageQueue.add(smessage);
        ctx.getCurrentActivity().runOnUiThread(controller);

    }

    /**
     * decrypts and decompresses incoming messages from PHP server
     * 
     * @param readBuffer
     *            compressed and encrypted message
     * @return decrypted and decompressed message
     * @throws Exception
     */
    public String decryptDecompressMessages(byte[] readBuffer) throws Exception {

        // CHECKED: decompress message
        ByteArrayInputStream bais = new ByteArrayInputStream(readBuffer);
        InputStreamReader isr = new InputStreamReader(bais, charsetCoding);
        StringBuffer converted = new StringBuffer();

        readingBuffer.clear();
        int bytesRead;
        while ((bytesRead = isr.read(readingBuffer)) != -1) {
            readingBuffer.rewind();
            converted.append(readingBuffer, 0, bytesRead);
            readingBuffer.clear();
        }

        String inputString = converted.toString();
        if (readBuffer.length % 16 != 0) {
            throw new Exception("Server error:" + inputString);
        }
        // CHECKED: decrypt and decompress the messages
        return decompressMessage(encrypter.decryptToBytes(readBuffer));
    }

    /**
     * Compresses the message for sending, to be read with PHP gzinflate()
     * 
     * @param out
     *            output stream in which compressed data will be contained
     * @param message
     *            message to be compressed
     * @throws IOException
     * @return bytes with compressed message
     */
    public byte[] compressMessage(String message) throws IOException {
        // CHECKED: compress message
        if (message.length() == 0)
            return new byte[0];
        ByteArrayOutputStream compressedMessageStream =
                new ByteArrayOutputStream(message.length() * 2);
        DeflaterOutputStream dos =
                new DeflaterOutputStream(compressedMessageStream);

        dos.write(message.getBytes(charsetCoding));
        dos.close();

        return compressedMessageStream.toByteArray();
    }

    /**
     * buffer used in decompression of messages
     */
    private static CharBuffer readingBuffer = CharBuffer.allocate(1024);

    /**
     * decompresses message using DEFLATE algorithm
     * 
     * @param in
     *            input stream to which decompressed message will be sent
     * @param compressedMessage
     *            byte array with compressed message
     * @throws IOException
     */
    public String decompressMessage(byte[] compressedMessage)
            throws IOException {
        if (compressedMessage.length == 0)
            return "";

        // CHECKED: decompress message
        ByteArrayInputStream bais = new ByteArrayInputStream(compressedMessage);
        InflaterInputStream iis = new InflaterInputStream(bais);
        InputStreamReader isr = new InputStreamReader(iis, charsetCoding);
        StringBuffer decompressed = new StringBuffer();

        readingBuffer.clear();
        int bytesRead;
        while ((bytesRead = isr.read(readingBuffer)) != -1) {
            readingBuffer.rewind();
            decompressed.append(readingBuffer, 0, bytesRead);
            readingBuffer.clear();
        }

        return decompressed.toString();
    }

    /**
     * compress and encrypt a message
     * 
     * @param message
     *            message to be compressed and encrypted
     * @return encrypted and compressed message in bytes
     * @throws Exception
     */
    public byte[] compressEncryptMessage(final String message) throws Exception {

        return encrypter.encryptToBytes(compressMessage(message));

    }

    /**
     * Causes sleep of your thread until all messages were sent
     * 
     * @param timeSeconds
     *            how much to wait for message sending
     * @return true iff messages were sent, false if timed out
     * @throws InterruptedException
     */
    public boolean waitForThreads(int timeSeconds) throws InterruptedException {
        // return this.threadPool.awaitTermination(timeSeconds,
        // TimeUnit.SECONDS);
        synchronized (this.threadPool) {
            this.threadPool.wait(timeSeconds * 1000);
        }
        return true;
    }

    @Override
    public void run() {
        // CHECK: send all messages from queue

        // CHECK:encode all messages to JSON
        synchronized (this.threadPool) {
            String encodedMessages = "";
            while ( !outboundMessageQueue.isEmpty()) {
                MessageFromApp message = outboundMessageQueue.poll();
                String json = message.getJSON();
                if ( !encodedMessages.isEmpty())
                    encodedMessages += ',';
                encodedMessages += json;
            }
            if ( !encodedMessages.isEmpty()) {
                encodedMessages = "[" + encodedMessages + "]";
                try {
                    sendAllMessages(encodedMessages);
                } catch (Exception e) {
                    UiController.addTask(taskID.applicationError, "Application error: " + e.toString());
                }
            }
            // let everyone know that this run has ended
            this.threadPool.notifyAll();
        }
    }

}
